package org.opens.util;

import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 功能:
 *
 * @author zym
 * @Date 2022/6/29 15:48
 */
@Component
public class RedisLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 获取锁
     * @param lockName 所名称
     * @param timeOut 锁的超时时间
     * @return 获取到的锁对应的token
     */
    public String lock(String lockName, Long timeOut) {
        Assert.isTrue(timeOut > 0, "超时时间必须大于0");
        long startTime = System.currentTimeMillis();
        String token = null;
        // 每隔50ms获取尝试一次锁, 模拟阻塞
        do {
            token = tryLock(lockName, timeOut);

            if (System.currentTimeMillis() - startTime > timeOut) {
                break;
            }

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return null;
            }
        } while (token == null);

        return token;
    }

    /**
     * 阻塞方式在超时时间内循环获取锁
     * @param lockName 锁名称
     * @param timeOut 锁超时时间
     * @return 锁对应的令牌
     */
    public String tryLock(String lockName, Long timeOut) {
        String token = UUID.randomUUID().toString();
        /**
         * setIfAbsent方法的机制:
         * - 如果为空就set值，并返回1
         * - 如果存在(不为空)不进行操作，并返回0
         * setIfAbsent 是java中的方法
         * setnx 是 redis命令中的方法
         */
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, token, timeOut, TimeUnit.MILLISECONDS);
        if (flag) {
            return token;
        }
        return null;
    }

    /**
     * 解锁操做, 在锁对象使用完毕后调用
     * @param lockName 锁名称
     * @param token 锁对应的令牌
     */
    public void unlock(String lockName, String token) {
        String lockToken = redisTemplate.opsForValue().get(lockName);
        if (StrUtil.equals(token, lockToken)) {
            redisTemplate.delete(lockName);
        }
    }

    /**
     * 检查行数执行时间是否大于锁超时时间
     *
     * 该方法必须调用于有事务的方法且不能try...catch..., 否则无效
     * @param startTime 函数执行开始使劲啊
     * @param timeOut 锁对象超时时间
     */
    public void checkRuntime(long startTime, long timeOut) {
        if (System.currentTimeMillis() - startTime > timeOut) {
            throw new RuntimeException("被锁方法执行超出锁超时时间!");
        }
    }

}
